require 'yaml'

class FoxifyFixtures

  class << self
  
    def foxify
      # foxify both traditional and rspec fixtures
      folders = Dir.glob("#{RAILS_ROOT}/test") + Dir.glob("#{RAILS_ROOT}/spec")
      folders.each do |folder|
        id_to_name = map_id_to_fixture_name(folder)

        table_names = id_to_name.keys

        foreign_key_to_reflection = map_foreign_key_to_reflection(table_names)

        data = load_data(folder)

        # Replace any occurences of:
        #   '<key name>: <record id>'
        #    --- with ---
        #   <table>: <record_name>'
        table_names.each do |table_name|
          data[table_name].each_key do |record_name|
            # get rid of pre-assigned id's, which appear to interfere with
            # foxy fixtures
            data[table_name][record_name].delete('id')

            data[table_name][record_name].each_key do |key|
              # look for foreign keys
              if( key =~ /\A(.*_id)\Z/ )
                foreign_key = $1
                reflection = foreign_key_to_reflection[table_name][foreign_key]

                # complain if an association seems to be missing
                if( reflection.nil? )
                  raise RuntimeError, "Foreign key '#{foreign_key}' for table " +
                    "'#{table_name}' appears to be missing an association in the " +
                    "model."
                else
                  referenced_table = reflection.klass.table_name

                  referenced_id = data[table_name][record_name][key]
                  if( referenced_id != nil &&
                      table_names.include?(referenced_table) )
                    referenced_id = referenced_id.to_i

                    # delete the existing id-based reference
                    data[table_name][record_name].delete(key)

                    # create the new name-based reference
                    data[table_name][record_name][reflection.name.to_s] =
                      id_to_name[referenced_table][referenced_id]
                  end
                end
              end
            end
          end
        end

        # Move original fixtures to a backup folder
        FileUtils.mkdir_p("#{folder}/fixtures/backup")
        FileUtils.mv( Dir.glob("#{folder}/fixtures/*.yml"),
                      "#{folder}/fixtures/backup")

        # Write out updated fixtures
        table_names.each do |table_name|
          File.open( "#{folder}/fixtures/#{table_name}.yml", 'w' ) do |out|
            #YAML.dump( data[table_name], out )
            out << data[table_name].to_yaml(:SortKeys => true)
          end
        end
      end
    end

    private

    def map_id_to_fixture_name(folder)
      id_to_name = Hash.new

      # First gather all mappings between id's and fixture names
      Dir["#{folder}/fixtures/*.yml"].each do |file|      
        table_name = File.basename(file, ".yml")

        if data = YAML::load_file(file)
          id_to_name[table_name] = Hash.new

          data.each do |record_name, record|
            if( record['id'] != nil )
              id_to_name[table_name][ record['id'].to_i ] =
                record_name
            end
          end
        end          
      end

      return id_to_name
    end

    def map_foreign_key_to_reflection(table_names)
      foreign_key_to_table = Hash.new

      table_names.each do |table_name|   
        class_name = table_name.classify.constantize
        class_name.reflect_on_all_associations(:belongs_to).each do |reflection|
          originating_table = reflection.active_record.table_name
          if(foreign_key_to_table[originating_table].nil?)
            foreign_key_to_table[originating_table] = Hash.new
          end

          foreign_key = reflection.primary_key_name

          foreign_key_to_table[originating_table][foreign_key] =
            reflection
        end
      end  
      return foreign_key_to_table
    end

    def load_data(folder)
      data = Hash.new

      # First gather all mappings between id's and fixture names
      Dir["#{folder}/fixtures/*.yml"].each do |file|      
        table_name = File.basename(file, ".yml")
        data[table_name] = YAML::load_file(file)
      end

      return data
    end
  
  end
end